---
title: "Part 3: Approval UI"
---

## Background: LangGraph implementation details

import Image from "next/image";
import approval from "./images/stockbroker-langgraph.png";

<Image
  src={approval}
  alt="LangChain LangGraph"
  width={600}
  className="mx-auto rounded-lg border shadow"
/>

Our LangGraph backend interrupts the `purchase_stock` tool execution in order to ensure the user confirms the purchase. The user confirms the purchase by submitting a tool message with the `approve` field set to `true`.

```ts title="assistant-ui-stockbroker/backend/src/index.ts" {6,18-19,32-35}
const purchaseApproval = async (state: typeof GraphAnnotation.State) => {
  const { messages } = state;
  const lastMessage = messages[messages.length - 1];
  if (!(lastMessage instanceof ToolMessage)) {
    // Interrupt the node to request permission to execute the purchase.
    throw new NodeInterrupt("Please confirm the purchase before executing.");
  }
};

const shouldExecutePurchase = (state: typeof GraphAnnotation.State) => {
  const { messages } = state;
  const lastMessage = messages[messages.length - 1];
  if (!(lastMessage instanceof ToolMessage)) {
    // Interrupt the node to request permission to execute the purchase.
    throw new NodeInterrupt("Please confirm the purchase before executing.");
  }

  const { approve } = JSON.parse(lastMessage.content as string);
  return approve ? "execute_purchase" : "agent";
};

const workflow = new StateGraph(GraphAnnotation)
  .addNode("agent", callModel)
  .addEdge(START, "agent")
  .addNode("tools", toolNode)
  .addNode("prepare_purchase_details", preparePurchaseDetails)
  .addNode("purchase_approval", purchaseApproval)
  .addNode("execute_purchase", executePurchase)
  .addEdge("prepare_purchase_details", "purchase_approval")
  .addEdge("execute_purchase", END)
  .addEdge("tools", "agent")
  .addConditionalEdges("purchase_approval", shouldExecutePurchase, [
    "agent",
    "execute_purchase",
  ])
  .addConditionalEdges("agent", shouldContinue, [
    "tools",
    END,
    "prepare_purchase_details",
  ]);
```

## Add approval UI

We create a new file under `/components/tools/purchase-stock/PurchaseStockTool.tsx` to define the tool.

First, we define the tool arguments and result types:

```ts title="@/components/tools/purchase-stock/PurchaseStockTool.tsx"
type PurchaseStockArgs = {
  ticker: string;
  companyName: string;
  quantity: number;
  maxPurchasePrice: number;
};

type PurchaseStockResult = {
  approve?: boolean;
  cancelled?: boolean;
  error?: string;
};
```

Then we use `makeAssistantToolUI` to define the tool UI:

```tsx title="@/components/tools/purchase-stock/PurchaseStockTool.tsx"
"use client";

import { TransactionConfirmationPending } from "./transaction-confirmation-pending";
import { TransactionConfirmationFinal } from "./transaction-confirmation-final";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { updateState } from "@/lib/chatApi";

export const PurchaseStockTool = makeAssistantToolUI<PurchaseStockArgs, string>(
  {
    toolName: "purchase_stock",
    render: function PurchaseStockUI({ args, result, status, addResult }) {
      const handleReject = async () => {
        addResult({ approve: false });
      };

      const handleConfirm = async () => {
        addResult({ approve: true });
      };

      return (
        <div className="mb-4 flex flex-col items-center gap-2">
          <div>
            <pre className="whitespace-pre-wrap break-all text-center">
              purchase_stock({JSON.stringify(args)})
            </pre>
          </div>
          {!result && status.type !== "running" && (
            <TransactionConfirmationPending
              {...args}
              onConfirm={handleConfirm}
              onReject={handleReject}
            />
          )}
        </div>
      );
    },
  },
);
```

Finally, we add a `TransactionConfirmationPending` component to ask for approval.

This requires shadcn/ui's `Card` and `Button` components. We will install them as a dependency.

```sh
npx shadcn@latest add card button
```

Then create a new file under `/components/tools/purchase-stock/transaction-confirmation-pending.tsx` to define the approval UI.

```tsx title="@/components/tools/purchase-stock/transaction-confirmation-pending.tsx"
"use client";

import { CheckIcon, XIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

type TransactionConfirmation = {
  ticker: string;
  companyName: string;
  quantity: number;
  maxPurchasePrice: number;
  onConfirm: () => void;
  onReject: () => void;
};

export function TransactionConfirmationPending(props: TransactionConfirmation) {
  const {
    ticker,
    companyName,
    quantity,
    maxPurchasePrice,
    onConfirm,
    onReject,
  } = props;

  return (
    <Card className="mx-auto w-full max-w-md">
      <CardHeader>
        <CardTitle className="text-2xl font-bold">
          Confirm Transaction
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="grid grid-cols-2 gap-2">
          <p className="text-muted-foreground text-sm font-medium">Ticker:</p>
          <p className="text-sm font-bold">{ticker}</p>
          <p className="text-muted-foreground text-sm font-medium">Company:</p>
          <p className="text-sm">{companyName}</p>
          <p className="text-muted-foreground text-sm font-medium">Quantity:</p>
          <p className="text-sm">{quantity} shares</p>
          <p className="text-muted-foreground text-sm font-medium">
            Max Purchase Price:
          </p>
          <p className="text-sm">${maxPurchasePrice?.toFixed(2)}</p>
        </div>
        <div className="bg-muted rounded-md p-3">
          <p className="text-sm font-medium">Total Maximum Cost:</p>
          <p className="text-lg font-bold">
            ${(quantity * maxPurchasePrice)?.toFixed(2)}
          </p>
        </div>
      </CardContent>
      <CardFooter className="flex justify-end">
        <Button variant="outline" onClick={onReject}>
          <XIcon className="mr-2 h-4 w-4" />
          Reject
        </Button>
        <Button onClick={onConfirm}>
          <CheckIcon className="mr-2 h-4 w-4" />
          Confirm
        </Button>
      </CardFooter>
    </Card>
  );
}
```

### Bind approval UI

```tsx title="@/app/page.tsx" {1,8}
import { PurchaseStockTool } from "@/components/tools/purchase-stock/PurchaseStockTool";

export default function Home() {
  return (
    <div className="flex h-full flex-col">
      <Thread
        ...
        tools={[PriceSnapshotTool, PurchaseStockTool]}
      />
    </div>
  );
}
```

### Try it out!

Ask the assistant to buy 5 shares of Tesla. You should see the following appear:

import purchase from "./images/acme-approve.png";

<Image
  src={purchase}
  alt="Approval UI"
  width={600}
  className="mx-auto rounded-lg border shadow"
/>

## Add `TransactionConfirmationFinal` to show approval result

We will add a component to display the approval result.

```ts title="@/components/tools/purchase-stock/transaction-confirmation-final.tsx"
"use client";

import { CheckCircle } from "lucide-react";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

type TransactionConfirmation = {
  ticker: string;
  companyName: string;
  quantity: number;
  maxPurchasePrice: number;
};

export function TransactionConfirmationFinal(props: TransactionConfirmation) {
  const { ticker, companyName, quantity, maxPurchasePrice } = props;

  return (
    <Card className="mx-auto w-full max-w-md">
      <CardHeader className="text-center">
        <CheckCircle className="mx-auto mb-4 h-16 w-16 text-green-500" />
        <CardTitle className="text-2xl font-bold text-green-700">
          Transaction Confirmed
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="rounded-md border border-green-200 bg-green-50 p-4">
          <h3 className="mb-2 text-lg font-semibold text-green-800">
            Purchase Summary
          </h3>
          <div className="grid grid-cols-2 gap-2 text-sm">
            <p className="font-medium text-green-700">Ticker:</p>
            <p className="font-bold text-green-900">{ticker}</p>
            <p className="font-medium text-green-700">Company:</p>
            <p className="text-green-900">{companyName}</p>
            <p className="font-medium text-green-700">Quantity:</p>
            <p className="text-green-900">{quantity} shares</p>
            <p className="font-medium text-green-700">Price per Share:</p>
            <p className="text-green-900">${maxPurchasePrice?.toFixed(2)}</p>
          </div>
        </div>
        <div className="rounded-md border border-green-300 bg-green-100 p-4">
          <p className="text-lg font-semibold text-green-800">Total Cost:</p>
          <p className="text-2xl font-bold text-green-900">
            ${(quantity * maxPurchasePrice)?.toFixed(2)}
          </p>
        </div>
        <p className="text-center text-sm text-green-600">
          Your purchase of {quantity} shares of {companyName} ({ticker}) has
          been successfully processed.
        </p>
      </CardContent>
    </Card>
  );
}
```

### Update `PurchaseStockTool`

We will import the new `<TransactionConfirmationFinal />` component and use it in the `render` function whenever an approval result is available.

```tsx title="@/components/tools/purchase-stock/PurchaseStockTool.tsx" {3,25-30,37-42}
"use client";

import { TransactionConfirmationPending } from "./transaction-confirmation-pending";
import { TransactionConfirmationFinal } from "./transaction-confirmation-final";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { updateState } from "@/lib/chatApi";

type PurchaseStockArgs = {
  ticker: string;
  companyName: string;
  quantity: number;
  maxPurchasePrice: number;
};

type PurchaseStockResult = {
  approve?: boolean;
  cancelled?: boolean;
  error?: string;
};

export const PurchaseStockTool = makeAssistantToolUI<PurchaseStockArgs, string>(
  {
    toolName: "purchase_stock",
    render: function PurchaseStockUI({ args, result, status, addResult }) {
      let resultObj: PurchaseStockResult;
      try {
        resultObj = result ? JSON.parse(result) : {};
      } catch (e) {
        resultObj = { error: result! };
      }

      const handleReject = () => {
        addResult({ cancelled: true });
      };

      const handleConfirm = async () => {
        addResult({ approve: true });
      };

      return (
        <div className="mb-4 flex flex-col items-center gap-2">
          <div>
            <pre className="whitespace-pre-wrap break-all text-center">
              purchase_stock({JSON.stringify(args)})
            </pre>
          </div>
          {!result && status.type !== "running" && (
            <TransactionConfirmationPending
              {...args}
              onConfirm={handleConfirm}
              onReject={handleReject}
            />
          )}
          {resultObj.approve && <TransactionConfirmationFinal {...args} />}
          {resultObj.approve === false && (
            <pre className="font-bold text-red-600">User rejected purchase</pre>
          )}
          {resultObj.cancelled && (
            <pre className="font-bold text-red-600">Cancelled</pre>
          )}
        </div>
      );
    },
  },
);
```

### Try it out!

Confirm the purchase of shares. You should see the approval confimration UI appear.

import purchase2 from "./images/acme-confirmed.png";

<Image
  src={purchase2}
  alt="Approval result"
  width={600}
  className="mx-auto rounded-lg border shadow"
/>
